package com.derekandbritt.booST.server;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.List;

import javax.jdo.JDOHelper;
import javax.jdo.JDOObjectNotFoundException;
import javax.jdo.PersistenceManager;
import javax.jdo.PersistenceManagerFactory;
import javax.jdo.Query;

import com.derekandbritt.booST.data.Challenge;
import com.derekandbritt.booST.data.EmotionLevel;
import com.derekandbritt.booST.data.EnergyLevel;
import com.derekandbritt.booST.data.User;
import com.derekandbritt.booST.data.UserStats;

public class DataManager {
	
	private static final PersistenceManagerFactory pmfInstance = JDOHelper.getPersistenceManagerFactory("transactions-optional");

    public static PersistenceManager getPersistenceManager() {
        return pmfInstance.getPersistenceManager();
    }
	
	public boolean isServiceOperational() {
		return true;
	}
	
	public void updateLastActivity(String userId) {
        if(userId != null && !userId.trim().isEmpty())
            updateUser(userId, true);
	}
	
	public boolean updateUser(String userId, boolean online) {
        
	    Date currentTime = new Date();
        
        PersistenceManager pm = getPersistenceManager();
        try { 
            User user = pm.getObjectById(User.class, userId);
            
            // if this is the first activity of the day then boost energy
            if(user.lastActivity.getTime() < CalendarUtil.getCurrentDay()) {
                EnergyLevel energy = new EnergyLevel(userId, 1);
                energy.setTimestamp(currentTime);
                setEnergyLevel(energy, pm);
            }
            
            // update the user's info
            user.setLastActivity(currentTime);
            user.setOnline(online);
            return true;
            
        } catch(JDOObjectNotFoundException e) {
            return false;
        } finally {
            pm.close();
        }
	}
	
	public User getUser(String userId, int flags) {
	    
		User user = null;
		
		PersistenceManager pm = getPersistenceManager();
		try { 
			user = pm.getObjectById(User.class, userId);
			pm.makeTransient(user);
		} catch(JDOObjectNotFoundException e) {
			
			// if the user doesn't exist then attempt to create one
			if((User.CREATE_IF_NEEDED_FLAG & flags) > 0) {
				user = new User();
				user.userID = userId;
				user.online = false;
				user.lastActivity = new Date(System.currentTimeMillis());
				pm.makePersistent(user);
				
				// energy boost for opening a new account
                EnergyLevel energy = new EnergyLevel(userId, 2);
                energy.setTimestamp(user.lastActivity);
                setEnergyLevel(energy, pm);
			}
			
		} finally {
		    if(user == null)
		        return null;
			pm.close();
		}
		
		if((User.INCLUDE_ENERGY_FLAG & flags) > 0) {
			user.currentEnergy = getCurrentEnergyLevel(userId);
			user.pastEnergy = getPastEnergyLevels(userId, 10);
			user.flags |= User.INCLUDE_ENERGY_FLAG;
		}
		
		if((User.INCLUDE_EMOTION_FLAG & flags) > 0) {
			user.currentEmotion = getEmotionLevel(userId);
			user.flags |= User.INCLUDE_EMOTION_FLAG;
		}
		
		if((User.INCLUDE_CHALLENGE_FLAG & flags) > 0) {
			user.activeChallenges = getActiveChallenges(userId);
			user.flags |= User.INCLUDE_CHALLENGE_FLAG;
		}
		
		if((User.INCLUDE_STATISTICS_FLAG & flags) > 0) {
		    
		    Date oneDay = new Date(System.currentTimeMillis() - 86400000);
		    Date tenDays = new Date(System.currentTimeMillis() - 864000000);
		    
            user.statistics = new UserStats();
            user.statistics.win10 = getChallengeWinCount(userId, 10);
            user.statistics.completed10 = getCompletedChallengeCount(userId, tenDays);
            user.statistics.active24 = getChallengeActivityMinutes(userId, oneDay);
            user.flags |= User.INCLUDE_STATISTICS_FLAG;
        }
		
		return user;
	}	
	
	public EnergyLevel getCurrentEnergyLevel(String userId) {
	    return EnergyCalculator.computeCurrentEnergyLevel(userId);
	}

    public List<EnergyLevel> getPastEnergyLevels(String userId, int count) {
        return EnergyCalculator.computePastEnergyLevels(userId, count);
    }
	
	public void setEnergyLevel(EnergyLevel energy) {
				
		PersistenceManager pm = getPersistenceManager();
		try { 
		    setEnergyLevel(energy, pm);
		} finally {
			pm.close();
		}
	}
	
	private void setEnergyLevel(EnergyLevel energy, PersistenceManager pm) {
	    pm.makePersistent(energy);
	}

	public EmotionLevel getEmotionLevel(String userId) { 
	    PersistenceManager pm = getPersistenceManager();
	    try {
	        return getEmotionLevel(userId, pm);
	    } finally {
	        pm.close();
	    }
	}
	
	@SuppressWarnings("unchecked")
	public EmotionLevel getEmotionLevel(String userId, PersistenceManager pm) {
			
		final Query query = pm.newQuery(EmotionLevel.class);
		query.setFilter("userId == userIdParam");
		query.setOrdering("timestamp desc");
		query.declareParameters("String userIdParam");		
		query.setRange(0, 1);
		List<EmotionLevel> result = (List<EmotionLevel>) query.execute(userId);
		if(result != null && result.size() > 0) {
		    EmotionLevel level = result.get(0);
		    pm.makeTransient(level);
		    return level;
		}
		else
			return null;
	}
	
	public void setEmotionLevel(EmotionLevel emotionLevel) {
		
		if(emotionLevel.getLevel() > 10)
			emotionLevel.setLevel(10);
		else if(emotionLevel.getLevel() < 0)
			emotionLevel.setLevel(0);
		
		PersistenceManager pm = getPersistenceManager();
		try { 
			pm.makePersistent(emotionLevel);
		} finally {
			pm.close();
		}
	}
	
	public Challenge getChallenge(long challengeId) {
		
		PersistenceManager pm = getPersistenceManager();
		try { 
			Challenge challenge = pm.getObjectById(Challenge.class, (Long)challengeId);
			if(challenge != null) {
			    pm.makeTransient(challenge);
			    inspectChallenge(challenge, pm);
                return challenge;
			}
			else
			    return null;
		} finally {
			pm.close();
		}
	}
	
	public int getChallengeWinCount(String userId, int count) {
	    List<Challenge> challenges = getCompletedChallenges(userId, count);
	    
	    if(challenges == null || challenges.size() == 0)
	        return 0;
	    
	    int counter = 0;
	    for(Challenge c : challenges) {
	        if(c.isWinner(userId))
	            counter++;
	    }
	    return counter;
	}
	
	@SuppressWarnings("unchecked")
	public List<Challenge> getActiveChallenges(String userId) {
		
		PersistenceManager pm = getPersistenceManager();

		final Query rQuery = pm.newQuery(Challenge.class);
        rQuery.setFilter("receiver == p_userId && (status == p_statusOne || status == p_statusTwo)");
        rQuery.setOrdering("timeIssued desc");
        rQuery.declareParameters("String p_userId, String p_statusOne, String p_statusTwo");
        List<Challenge> rResult = (List<Challenge>) rQuery.execute(userId, Challenge.Status.PENDING, Challenge.Status.ACCEPTED);
        pm.makeTransientAll(rResult);
		
		final Query sQuery = pm.newQuery(Challenge.class);
        sQuery.setFilter("sender == p_userId && (status == p_statusOne || status == p_statusTwo)");
        sQuery.setOrdering("timeIssued desc");
        sQuery.declareParameters("String p_userId, String p_statusOne, String p_statusTwo");
        List<Challenge> sResult = (List<Challenge>) sQuery.execute(userId, Challenge.Status.PENDING, Challenge.Status.ACCEPTED);
		pm.makeTransientAll(sResult);
		
		pm.close();
		
		if(rResult.size() > 0 || sResult.size() > 0) {
		    ArrayList results = new ArrayList<Challenge>(rResult);
		    results.addAll(sResult);
		    
		    // if there are changes then commit the changes are redo the operation
            if(!inspectChallenges(results)) {
                return getActiveChallenges(userId);
            }
		    
			Collections.sort(results);
			return results;
		}
		else
			return null;
	}

	@SuppressWarnings("unchecked")
    public List<Challenge> getCompletedChallenges(String userId, int count) {
        
        PersistenceManager pm = getPersistenceManager();
            
        final Query rQuery = pm.newQuery(Challenge.class);
        rQuery.setFilter("receiver == p_userId && (status == p_statusOne || status == p_statusTwo)");
        rQuery.setOrdering("timeIssued desc");
        rQuery.declareParameters("String p_userId, String p_statusOne, String p_statusTwo");
        rQuery.setRange(0, count);
        List<Challenge> rResult = (List<Challenge>) rQuery.execute(userId, Challenge.Status.COMPLETED, Challenge.Status.REJECTED);
        pm.makeTransientAll(rResult);
        
        final Query sQuery = pm.newQuery(Challenge.class);
        sQuery.setFilter("sender == p_userId && (status == p_statusOne || status == p_statusTwo)");
        sQuery.setOrdering("timeIssued desc");
        sQuery.declareParameters("String p_userId, String p_statusOne, String p_statusTwo");
        sQuery.setRange(0, count);
        List<Challenge> sResult = (List<Challenge>) sQuery.execute(userId, Challenge.Status.COMPLETED, Challenge.Status.REJECTED);
        pm.makeTransientAll(sResult);
        
        pm.close();
        
        if(rResult.size() > 0 || sResult.size() > 0) {
            ArrayList results = new ArrayList<Challenge>(rResult);
            results.addAll(sResult);
            
            // if there are changes then commit the changes are redo the operation
            if(!inspectChallenges(results)) {
                return getCompletedChallenges(userId, count);
            }
            
            Collections.sort(results);
            return (count > results.size()) ? results : results.subList(0, count-1);
        }
        else
            return null;
    }
	
    public int getCompletedChallengeCount(String userId, Date startDate) {
        
        PersistenceManager pm = getPersistenceManager();
            
        final Query rQuery = pm.newQuery(Challenge.class);
        rQuery.setFilter("receiver == p_userId && status == p_status && timeIssued > p_time");
        rQuery.declareImports("import java.util.Date");
        rQuery.declareParameters("String p_userId, String p_status, Date p_time");
        rQuery.setResult("count(id)");
        rQuery.setResultClass(Integer.class);
        Integer rResult = (Integer) rQuery.execute(userId, Challenge.Status.COMPLETED, startDate);
        
        final Query sQuery = pm.newQuery(Challenge.class);
        sQuery.setFilter("sender == p_userId && status == p_status && timeIssued > p_time");
        sQuery.declareImports("import java.util.Date");
        sQuery.declareParameters("String p_userId, String p_status, Date p_time");
        sQuery.setResult("count(id)");
        sQuery.setResultClass(Integer.class);
        Integer sResult = (Integer) sQuery.execute(userId, Challenge.Status.COMPLETED, startDate);
        
        pm.close();
        
        return rResult.intValue() + sResult.intValue();
    }
	
    @SuppressWarnings("unchecked")
    public int getChallengeActivityMinutes(String userId, Date startDate) {
        
        PersistenceManager pm = getPersistenceManager();
        
        final Query rQuery = pm.newQuery(Challenge.class);
        rQuery.setFilter("receiver == userIdParam && lastUpdated > p_time");
        rQuery.declareImports("import java.util.Date");
        rQuery.declareParameters("String p_userId, Date p_time");
        List<Challenge> rResult = (List<Challenge>) rQuery.execute(userId, startDate);
        
        final Query sQuery = pm.newQuery(Challenge.class);
        sQuery.setFilter("sender == userIdParam && lastUpdated > p_time");
        sQuery.declareImports("import java.util.Date");
        sQuery.declareParameters("String p_userId, Date p_time");
        List<Challenge> sResult = (List<Challenge>) rQuery.execute(userId, startDate);
        
        long time = 0;
        
        if(rResult != null) {
            for(Challenge c : rResult)
                time += c.getReceiverTime();
        }
        
        if(sResult != null) {
            for(Challenge c : sResult)
                time += c.getSenderTime();
        }
        
        pm.close();
        return (time > 0) ? (int)(time / 1000) : 0;
    }
    
	@SuppressWarnings("unchecked")
	public List<Challenge> getChallenges(String userId, int count) {
		
		PersistenceManager pm = getPersistenceManager();
		    
	    final Query rQuery = pm.newQuery(Challenge.class);
        rQuery.setFilter("receiver == userIdParam");
        rQuery.setOrdering("timeIssued desc");
        rQuery.declareParameters("String userIdParam");
        rQuery.setRange(0, count);
        List<Challenge> rResult = (List<Challenge>) rQuery.execute(userId);
        pm.makeTransientAll(rResult);
        
        final Query sQuery = pm.newQuery(Challenge.class);
        sQuery.setFilter("sender == userIdParam");
        sQuery.setOrdering("timeIssued desc");
        sQuery.declareParameters("String userIdParam");
        sQuery.setRange(0, count);
        List<Challenge> sResult = (List<Challenge>) sQuery.execute(userId);
        pm.makeTransientAll(sResult);
        
        pm.close();
        
        if(rResult.size() > 0 || sResult.size() > 0) {
            ArrayList results = new ArrayList<Challenge>(rResult);
            results.addAll(sResult);
            
            // if there are changes then commit the changes are redo the operation
            if(!inspectChallenges(results)) {
                return getChallenges(userId, count);
            }
            
            Collections.sort(results);
            return (count > results.size()) ? results : results.subList(0, count-1);
        }
        else
            return null;
		
	}

	private boolean inspectChallenges(List<Challenge> challenges) {
	    boolean noChanges = true;
	    
	    PersistenceManager pm = getPersistenceManager();
	    for(Challenge challenge : challenges) {
	        if(!inspectChallenge(challenge, pm))
	            noChanges = false;
	    }
	    pm.close();
	    
	    return noChanges;
	}
	
	private boolean inspectChallenge(Challenge challenge, PersistenceManager pm) {
	    
	    long currentTime = System.currentTimeMillis();
	    
	    if(challenge.getStatus() == Challenge.Status.PENDING &&
	            currentTime > challenge.getExpiration().getTime()) {
	        
	        // get the attached version of the challenge and update it
	        Challenge attachedChallenge = pm.getObjectById(Challenge.class, (Long)challenge.getId());
            attachedChallenge.setStatus(Challenge.Status.EXPIRED);
            attachedChallenge.setLastUpdated(new Date(currentTime));
            
            // update the input parameter in case it is returned
            challenge.setStatus(Challenge.Status.EXPIRED);
            challenge.setLastUpdated(new Date(currentTime));
            return false;
	    }
	    else if(challenge.getStatus() == Challenge.Status.ACCEPTED &&
                currentTime > challenge.getExpiration().getTime()) {
	        
	        //declare a winner or both losers depending on the state
	        boolean senderWon = (challenge.getSenderTime() <= 0) ? true : false;
	        boolean receiverWon = (challenge.getReceiverTime() <= 0) ? true : false;
	        
	        Challenge.Status status = Challenge.Status.EXPIRED;
	        if(senderWon || receiverWon) 
	            status = Challenge.Status.COMPLETED;
	        
	        // get the attached version of the challenge and update it
            Challenge attachedChallenge = pm.getObjectById(Challenge.class, (Long)challenge.getId());
            attachedChallenge.setStatus(status);
            attachedChallenge.setLastUpdated(new Date(currentTime));
            
            // update the input parameter in case it is returned
            challenge.setStatus(status);
            challenge.setLastUpdated(new Date(currentTime));

            // store the resulting changes to the participant's energyLevel
            ChallengeCalculator calc = new ChallengeCalculator(this, pm);
            for(EnergyLevel level : calc.evaluateChallenge(challenge, attachedChallenge)) {
                setEnergyLevel(level, pm);
            }
            
	        return false;
	    }
	    
	    return true;
	}
	
	public void setChallenge(Challenge challenge) {
		
	    Challenge oldChallenge = null;
	    Challenge newChallenge = null;
	    
		PersistenceManager pm = getPersistenceManager();		
		try {
			
			if(challenge.getId() > 0) {
		
			    newChallenge = pm.getObjectById(Challenge.class, (Long) challenge.getId());
				if(newChallenge == null)
					throw new IllegalStateException("Challenge has an invalid id");
				
				oldChallenge = pm.detachCopy(newChallenge);
				
				newChallenge.setLastUpdated(challenge.getLastUpdated());
				newChallenge.setStatus(challenge.getStatus());
				newChallenge.setSenderTime(challenge.getSenderTime());
				newChallenge.setReceiverTime(challenge.getReceiverTime());
				pm.makeTransient(newChallenge);
			}
			else {
			    newChallenge = pm.makePersistent(challenge);
			}
		} finally {
			pm.close();
		}
		
		// store the resulting changes to the participant's energyLevel
		ChallengeCalculator calc = new ChallengeCalculator(this);
        for(EnergyLevel level : calc.evaluateChallenge(oldChallenge, newChallenge)) {
            setEnergyLevel(level);
        }
	}
}